import random
import time

import torch
import torch.nn.functional as F
import torchvision
from torch import nn
import gym
import numpy as np
import sounddevice as sd
import soundfile as sf
from gym import spaces
from stable_baselines3.ddpg.ddpg import DDPG
import matplotlib.pyplot as plt

from simpledaw import SimpleDAW
from sim_net import Dis, constractive_loss
from dataset import Audio2ParamsDataset, load_audio_as_spec, get_files

#####################  hyper parameters  ####################
EPISODES = 200
EP_STEPS = 5
LR_ACTOR = 0.001
LR_CRITIC = 0.002
GAMMA = 0.9
TAU = 0.01
MEMORY_CAPACITY = 100
BATCH_SIZE = 8
RENDER = False

cwd = 'C:/dev_spa/DMuse/202202b5'
    
config = {
        'dataset_config': {
            'train_sample_dir': cwd,
            'test_sample_dir': cwd,
            'train_label_dir': cwd,
            'test_label_dir': cwd,
            'compress_rate': 100.,
        },
        'batch_size': BATCH_SIZE,
        'shuffle': True,
    }

dataset = Audio2ParamsDataset(mode='train', **config['dataset_config'])


class Simulator(gym.Env):
    def __init__(self, env_shape, n_params, target, label):
        self.action_space = spaces.Box(high=1., low=0., shape=(n_params,))
        self.observation_space = spaces.Box(high=1., low=0., shape=env_shape)
        self.step_count = 0
        self.state = np.zeros(env_shape)
        self.target = target
        self.label = label
        self.env_shape = env_shape
        
        self.dis = Dis(env_shape)
        self.dis_optimizer = torch.optim.Adam(self.dis.parameters(), lr=0.0003)
        
        self.daw = SimpleDAW(plugin='C:/VST/64bit/Sylenth1.dll', sample_rate=44100, bpm=120)
        self.midi_paths = get_files(r'B:\muse_repo\MIDI', 'mid')
        self.daw.load_midi(random.choice(self.midi_paths))

    def step(self, action):
        self.daw.set_params(list(action))
        # print('正在播放预测声音')
        sf.write('./img2/{}.wav'.format(self.step_count), self.daw.render(2.), 44100)
        # print('播放结束')
        self.state = load_audio_as_spec('', 44100, config['dataset_config']['compress_rate'], raw=self.daw.render(2.))
        self.state = self.state.reshape((1,) + self.state.shape)
        # print(state.shape)
        
        output = self.dis(torch.Tensor(np.array(self.state)), self.target)
        self.dis_optimizer.zero_grad()
        loss = constractive_loss(self.label, output)
        loss.backward()
        self.dis_optimizer.step()
        reward = torch.mean(output.detach())
        
        self.step_count += 1
        print('[episode: {}]'.format(self.step_count), 'action:', action[:10], 'reward:', reward)
        
        done = True
        info = {}
        return self.state, reward, done, info

    def reset(self):
        self.state = np.zeros(self.env_shape)
        return self.state

    def render(self, mode='human'):
        pass

    def seed(self, seed=None):
        pass



########################## DDPG Framework ######################
class ActorNet(nn.Module): # define the network structure for actor and critic
    def __init__(self, s_shape, a_dim):
        super(ActorNet, self).__init__()
        '''self.layer = nn.Sequential(
            nn.Conv2d(3, 3, kernel_size=3),
            nn.ReLU(),
            nn.AvgPool2d(kernel_size=2, stride=2)
        )'''
        self.layer = torchvision.models.vgg16()
        
        self.fc1 = nn.Linear(25088, 30)
        self.fc1.weight.data.normal_(0, 0.1) # initialization of FC1
        self.out = nn.Linear(30, a_dim)
        self.out.weight.data.normal_(0, 0.1) # initilizaiton of OUT
        
    def forward(self, x):
        x = torch.transpose(x, 1, 3)
        x = self.layer.features(x)
        x = self.layer.avgpool(x)
        x = torch.flatten(x, 1)
        x = self.fc1(x)
        x = F.relu(x)
        x = self.out(x)
        x = torch.tanh(x)
        actions = x * 2 # for the game "Pendulum-v0", action range is [-2, 2]
        return actions

class CriticNet(nn.Module):
    def __init__(self, s_shape, a_dim):
        super(CriticNet, self).__init__()
        self.fcs = nn.Linear(25088, 30)
        self.fcs.weight.data.normal_(0, 0.1)
        self.fca = nn.Linear(a_dim, 30)
        self.fca.weight.data.normal_(0, 0.1)
        self.out = nn.Linear(30, 1)
        self.out.weight.data.normal_(0, 0.1)
        
    def forward(self, s, a):
        s = torch.transpose(s, 1, 3)
        s = self.layer.features(s)
        s = self.layer.avgpool(s)
        s = torch.flatten(s, 1)
        x = self.fcs(s)
        y = self.fca(a)
        actions_value = self.out(F.relu(x+y))
        return actions_value
    
class DDPG(object):
    def __init__(self, a_dim, s_shape, a_bound):
        self.a_dim, self.s_shape, self.a_bound = a_dim, s_shape, a_bound
        self.memory = [None, ] * MEMORY_CAPACITY
        self.pointer = 0 # serves as updating the memory data 
        # Create the 4 network objects
        self.actor_eval = ActorNet(s_shape, a_dim)
        self.actor_target = ActorNet(s_shape, a_dim)
        self.critic_eval = CriticNet(s_shape, a_dim)
        self.critic_target = CriticNet(s_shape, a_dim)
        # create 2 optimizers for actor and critic
        self.actor_optimizer = torch.optim.Adam(self.actor_eval.parameters(), lr=LR_ACTOR)
        self.critic_optimizer = torch.optim.Adam(self.critic_eval.parameters(), lr=LR_CRITIC)
        # Define the loss function for critic network update
        self.loss_func = nn.MSELoss()
    def store_transition(self, s, a, r, s_): # how to store the episodic data to buffer
        transition = (s, a, [r], s_)
        index = self.pointer % MEMORY_CAPACITY # replace the old data with new data 
        self.memory[index] = transition
        self.pointer += 1
    
    def choose_action(self, s):
        # print(s)
        s = torch.FloatTensor(s)
        return self.actor_eval(s)[0].detach()
    
    def learn(self):
        # softly update the target networks
        for x in self.actor_target.state_dict().keys():
            eval('self.actor_target.' + x + '.data.mul_((1-TAU))')
            eval('self.actor_target.' + x + '.data.add_(TAU*self.actor_eval.' + x + '.data)')
        for x in self.critic_target.state_dict().keys():
            eval('self.critic_target.' + x + '.data.mul_((1-TAU))')
            eval('self.critic_target.' + x + '.data.add_(TAU*self.critic_eval.' + x + '.data)')           
        # sample from buffer a mini-batch data
        indices = np.random.choice(MEMORY_CAPACITY, size=BATCH_SIZE)
        batch_trans = self.memory[indices]
        # extract data from mini-batch of transitions including s, a, r, s_
        batch_s = torch.FloatTensor(batch_trans[:self.s_shape[1]])
        batch_a = torch.FloatTensor(batch_trans[self.s_shape[1]:self.s_shape[1] + self.a_dim])
        batch_r = torch.FloatTensor(batch_trans[-self.s_shape[1] - 1: -self.s_shape[1]])
        batch_s_ = torch.FloatTensor(batch_trans[-self.s_shape[1]:])
        # make action and evaluate its action values
        a = self.actor_eval(batch_s)
        q = self.critic_eval(batch_s, a)
        actor_loss = -torch.mean(q)
        # optimize the loss of actor network
        self.actor_optimizer.zero_grad()
        actor_loss.backward()
        self.actor_optimizer.step()
        
        # compute the target Q value using the information of next state
        a_target = self.actor_target(batch_s_)
        q_tmp = self.critic_target(batch_s_, a_target)
        q_target = batch_r + GAMMA * q_tmp
        # compute the current q value and the loss
        q_eval = self.critic_eval(batch_s, batch_a)
        td_error = self.loss_func(q_target, q_eval)
        # optimize the loss of critic network
        self.critic_optimizer.zero_grad()
        td_error.backward()
        self.critic_optimizer.step()
        
############################### Training ######################################
# Define the env in gym

dataloader = torch.utils.data.DataLoader(
        dataset=dataset, batch_size=config['batch_size'], shuffle=True
    )


x, y = iter(dataloader).__next__()

env = Simulator(x.shape, dataset.n_params, x, y)

s_shape = env.observation_space.shape
a_dim = env.action_space.shape[0]
a_bound = env.action_space.high
a_low_bound = env.action_space.low

ddpg = DDPG(a_dim, s_shape, a_bound)
var = 3 # the controller of exploration which will decay during training process
t1 = time.time()
for i in range(EPISODES):
    s = env.reset()
    ep_r = 0
    for j in range(EP_STEPS):
        # add explorative noise to action
        print(s.shape)
        a = ddpg.choose_action(s)
        a = np.clip(np.random.normal(a, var), a_low_bound, a_bound)
        s_, r, done, info = env.step(a)
        ddpg.store_transition(s, a, r / 10, s_) # store the transition to memory
        
        
        if ddpg.pointer > MEMORY_CAPACITY:
            var *= 0.9995 # decay the exploration controller factor
            ddpg.learn()
            
        s = s_
        ep_r += r
        if j == EP_STEPS - 1:
            print('Episode: ', i, ' Reward: %i' % (ep_r), 'Explore: %.2f' % var)
            if ep_r > -300 : RENDER = True
            break
    
        plt.imshow(s[0, ...])
        plt.savefig('./img2/{}_{}.png'.format(i, j))
print('Running time: ', time.time() - t1)
    

